/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.view.numberpicker;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.NumberKeyListener;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.LayoutInflater.Filter;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.star.demo.R;

import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

//import android.annotation.Widget;

/**
 * A widget that enables the user to select a number form a predefined range.
 * There are two flavors of this widget and which one is presented to the user
 * depends on the current theme.
 * <ul>
 * <li>
 * If the current theme is derived from {@link android.R.style#Theme} the widget
 * presents the current value as an editable input field with an increment
 * button above and a decrement button below. Long pressing the buttons allows
 * for a quick change of the current value. Tapping on the input field allows to
 * type in a desired value.</li>
 * <li>
 * If the current theme is derived from {@link android.R.style#Theme_Holo} or
 * {@link android.R.style#Theme_Holo_Light} the widget presents the current
 * value as an editable input field with a lesser value above and a greater
 * value below. Tapping on the lesser or greater value selects it by animating
 * the number axis up or down to make the chosen value current. Flinging up or
 * down allows for multiple increments or decrements of the current value. Long
 * pressing on the lesser and greater values also allows for a quick change of
 * the current value. Tapping on the current value allows to type in a desired
 * value.</li>
 * </ul>
 * <p>
 * For an example of using this widget, see {@link android.widget.TimePicker}.
 * </p>
 */
//@Widget
public class NumberPicker extends LinearLayout
{
    
    /**
     * The number of items show in the selector wheel.
     */
    private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
    
    /**
     * The default update interval during long press.
     */
    private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
    
    /**
     * The index of the middle selector item.
     */
    private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
    
    /**
     * The coefficient by which to adjust (divide) the max fling velocity.
     */
    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
    
    /**
     * The the duration for adjusting the selector wheel.
     */
    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
    
    /**
     * The duration of scrolling while snapping to a given position.
     */
    private static final int SNAP_SCROLL_DURATION = 300;
    
    /**
     * The strength of fading in the top and bottom while drawing the selector.
     */
    private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
    
    /**
     * The default unscaled height of the selection divider.
     */
    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
    
    /**
     * The default unscaled distance between the selection dividers.
     */
    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
    
    /**
     * The resource id for the default layout.
     */
    private static final int DEFAULT_LAYOUT_RESOURCE_ID = 0;
    
    /**
     * Constant for unspecified size.
     */
    private static final int SIZE_UNSPECIFIED = -1;
    
    /**
     * Use a custom NumberPicker formatting callback to use two-digit minutes
     * strings like "01". Keeping a static formatter etc. is the most efficient
     * way to do this; it avoids creating temporary objects on every call to
     * format().
     */
    private static class TwoDigitFormatter implements NumberPicker.Formatter
    {
        final StringBuilder mBuilder = new StringBuilder();
        
        char mZeroDigit;
        
        java.util.Formatter mFmt;
        
        final Object[] mArgs = new Object[1];
        
        TwoDigitFormatter()
        {
            final Locale locale = Locale.getDefault();
            init(locale);
        }
        
        private void init(Locale locale)
        {
            mFmt = createFormatter(locale);
            mZeroDigit = getZeroDigit(locale);
        }
        
        public String format(int value)
        {
            final Locale currentLocale = Locale.getDefault();
            if (mZeroDigit != getZeroDigit(currentLocale))
            {
                init(currentLocale);
            }
            mArgs[0] = value;
            mBuilder.delete(0, mBuilder.length());
            mFmt.format("%02d", mArgs);
            return mFmt.toString();
        }
        
        private static char getZeroDigit(Locale locale)
        {
            // return LocaleData.get(locale).zeroDigit;
            return new DecimalFormatSymbols(locale).getZeroDigit();
        }
        
        private java.util.Formatter createFormatter(Locale locale)
        {
            return new java.util.Formatter(mBuilder, locale);
        }
    }
    
    private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter();
    
    /**
     * @hide
     */
    public static final Formatter getTwoDigitFormatter()
    {
        return sTwoDigitFormatter;
    }
    
    /**
     * The increment button.
     */
    private final ImageButton mIncrementButton;
    
    /**
     * The decrement button.
     */
    private final ImageButton mDecrementButton;
    
    /**
     * The text for showing the current value.
     */
    private final EditText mInputText;
    
    /**
     * The distance between the two selection dividers.
     */
    private final int mSelectionDividersDistance;
    
    /**
     * The min height of this widget.
     */
    private final int mMinHeight;
    
    /**
     * The max height of this widget.
     */
    private final int mMaxHeight;
    
    /**
     * The max width of this widget.
     */
    private final int mMinWidth;
    
    /**
     * The max width of this widget.
     */
    private int mMaxWidth;
    
    /**
     * Flag whether to compute the max width.
     */
    private final boolean mComputeMaxWidth;
    
    /**
     * The height of the text.
     */
    private final int mTextSize;
    
    /**
     * The height of the gap between text elements if the selector wheel.
     */
    private int mSelectorTextGapHeight;
    
    /**
     * The values to be displayed instead the indices.
     */
    private String[] mDisplayedValues;
    
    /**
     * Lower value of the range of numbers allowed for the NumberPicker
     */
    private int mMinValue;
    
    /**
     * Upper value of the range of numbers allowed for the NumberPicker
     */
    private int mMaxValue;
    
    /**
     * Current value of this NumberPicker
     */
    private int mValue;
    
    /**
     * Listener to be notified upon current value change.
     */
    private OnValueChangeListener mOnValueChangeListener;
    
    /**
     * Listener to be notified upon scroll state change.
     */
    private OnScrollListener mOnScrollListener;
    
    /**
     * Formatter for for displaying the current value.
     */
    private Formatter mFormatter;
    
    /**
     * The speed for updating the value form long press.
     */
    private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
    
    /**
     * Cache for the string representation of selector indices.
     */
    private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
    
    /**
     * The selector indices whose value are show by the selector.
     */
    private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
    
    /**
     * The {@link Paint} for drawing the selector.
     */
    private final Paint mSelectorWheelPaint;
    
    /**
     * The {@link Drawable} for pressed virtual (increment/decrement) buttons.
     */
    private final Drawable mVirtualButtonPressedDrawable;
    
    /**
     * The height of a selector element (text + gap).
     */
    private int mSelectorElementHeight;
    
    /**
     * The initial offset of the scroll selector.
     */
    private int mInitialScrollOffset = Integer.MIN_VALUE;
    
    /**
     * The current offset of the scroll selector.
     */
    private int mCurrentScrollOffset;
    
    /**
     * The {@link Scroller} responsible for flinging the selector.
     */
    private final Scroller mFlingScroller;
    
    /**
     * The {@link Scroller} responsible for adjusting the selector.
     */
    private final Scroller mAdjustScroller;
    
    /**
     * The previous Y coordinate while scrolling the selector.
     */
    private int mPreviousScrollerY;
    
    /**
     * Handle to the reusable command for setting the input text selection.
     */
    private SetSelectionCommand mSetSelectionCommand;
    
    /**
     * Handle to the reusable command for changing the current value from long
     * press by one.
     */
    private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
    
    /**
     * Command for beginning an edit of the current value via IME on long press.
     */
    private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
    
    /**
     * The Y position of the last down event.
     */
    private float mLastDownEventY;
    
    /**
     * The time of the last down event.
     */
    private long mLastDownEventTime;
    
    /**
     * The Y position of the last down or move event.
     */
    private float mLastDownOrMoveEventY;
    
    /**
     * Determines speed during touch scrolling.
     */
    private VelocityTracker mVelocityTracker;
    
    /**
     * @see ViewConfiguration#getScaledTouchSlop()
     */
    private int mTouchSlop;
    
    /**
     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
     */
    private int mMinimumFlingVelocity;
    
    /**
     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
     */
    private int mMaximumFlingVelocity;
    
    /**
     * Flag whether the selector should wrap around.
     */
    private boolean mWrapSelectorWheel;
    
    /**
     * The back ground color used to optimize scroller fading.
     */
    private final int mSolidColor;
    
    /**
     * Flag whether this widget has a selector wheel.
     */
    private final boolean mHasSelectorWheel;
    
    /**
     * Divider for showing item to be selected while scrolling
     */
    private final Drawable mSelectionDivider;
    
    /**
     * The height of the selection divider.
     */
    private final int mSelectionDividerHeight;
    
    /**
     * The current scroll state of the number picker.
     */
    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
    
    /**
     * Flag whether to ignore move events - we ignore such when we show in IME
     * to prevent the content from scrolling.
     */
    private boolean mIngonreMoveEvents;
    
    /**
     * Flag whether to show soft input on tap.
     */
    private boolean mShowSoftInputOnTap;
    
    /**
     * The top of the top selection divider.
     */
    private int mTopSelectionDividerTop;
    
    /**
     * The bottom of the bottom selection divider.
     */
    private int mBottomSelectionDividerBottom;
    
    /**
     * The virtual id of the last hovered child.
     */
    private int mLastHoveredChildVirtualViewId;
    
    /**
     * Whether the increment virtual button is pressed.
     */
    private boolean mIncrementVirtualButtonPressed;
    
    /**
     * Whether the decrement virtual button is pressed.
     */
    private boolean mDecrementVirtualButtonPressed;
    
    /**
     * Provider to report to clients the semantic structure of this widget.
     */
    private SupportAccessibilityNodeProvider mAccessibilityNodeProvider;
    
    /**
     * Helper class for managing pressed state of the virtual buttons.
     */
    private final PressedStateHelper mPressedStateHelper;
    
    /**
     * The keycode of the last handled DPAD down event.
     */
    private int mLastHandledDownDpadKeyCode = -1;
    
    /**
     * Interface to listen for changes of the current value.
     */
    public interface OnValueChangeListener
    {
        
        /**
         * Called upon a change of the current value.
         * 
         * @param picker
         *            The NumberPicker associated with this listener.
         * @param oldVal
         *            The previous value.
         * @param newVal
         *            The new value.
         */
        void onValueChange(NumberPicker picker, int oldVal, int newVal);
    }
    
    /**
     * Interface to listen for the picker scroll state.
     */
    public interface OnScrollListener
    {
        
        /**
         * The view is not scrolling.
         */
        int SCROLL_STATE_IDLE = 0;
        
        /**
         * The user is scrolling using touch, and his finger is still on the
         * screen.
         */
        int SCROLL_STATE_TOUCH_SCROLL = 1;
        
        /**
         * The user had previously been scrolling using touch and performed a
         * fling.
         */
        int SCROLL_STATE_FLING = 2;
        
        /**
         * Callback invoked while the number picker scroll state has changed.
         * 
         * @param view
         *            The view whose scroll state is being reported.
         * @param scrollState
         *            The current scroll state. One of
         *            {@link #SCROLL_STATE_IDLE},
         *            {@link #SCROLL_STATE_TOUCH_SCROLL} or
         *            {@link #SCROLL_STATE_IDLE}.
         */
        void onScrollStateChange(NumberPicker view, int scrollState);
    }
    
    /**
     * Interface used to format current value into a string for presentation.
     */
    public interface Formatter
    {
        
        /**
         * Formats a string representation of the current value.
         * 
         * @param value
         *            The currently selected value.
         * @return A formatted string representation.
         */
        String format(int value);
    }
    
    /**
     * Create a new number picker.
     * 
     * @param context
     *            The application environment.
     */
    public NumberPicker(Context context)
    {
        this(context, null);
    }
    
    /**
     * Create a new number picker.
     * 
     * @param context
     *            The application environment.
     * @param attrs
     *            A collection of attributes.
     */
    public NumberPicker(Context context, AttributeSet attrs)
    {
        this(context, attrs, R.attr.numberPickerStyle);
    }
    
    /**
     * Create a new number picker
     * 
     * @param context
     *            the application environment.
     * @param attrs
     *            a collection of attributes.
     * @param defStyle
     *            The default style to apply to this view.
     */
    public NumberPicker(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs);
        attrs.getAttributeResourceValue(null, "Text", 0);
        // process style attributes
        TypedArray attributesArray = context.obtainStyledAttributes(attrs,
                R.styleable.NumberPicker,
                defStyle,
                0);
        final int layoutResId = attributesArray.getResourceId(R.styleable.NumberPicker_internalLayout,
                DEFAULT_LAYOUT_RESOURCE_ID);
        
        mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
        
        mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor,
                0);
        
        mSelectionDivider = attributesArray.getDrawable(R.styleable.NumberPicker_selectionDivider);
        
        final int defSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
                getResources().getDisplayMetrics());
        mSelectionDividerHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_selectionDividerHeight,
                defSelectionDividerHeight);
        
        final int defSelectionDividerDistance = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
                getResources().getDisplayMetrics());
        mSelectionDividersDistance = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_selectionDividersDistance,
                defSelectionDividerDistance);
        
        mMinHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMinHeight,
                SIZE_UNSPECIFIED);
        
        mMaxHeight = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMaxHeight,
                SIZE_UNSPECIFIED);
        if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
                && mMinHeight > mMaxHeight)
        {
            throw new IllegalArgumentException("minHeight > maxHeight");
        }
        
        mMinWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMinWidth,
                SIZE_UNSPECIFIED);
        
        mMaxWidth = attributesArray.getDimensionPixelSize(R.styleable.NumberPicker_internalMaxWidth,
                SIZE_UNSPECIFIED);
        if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
                && mMinWidth > mMaxWidth)
        {
            throw new IllegalArgumentException("minWidth > maxWidth");
        }
        
        mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
        
        mVirtualButtonPressedDrawable = attributesArray.getDrawable(R.styleable.NumberPicker_virtualButtonPressedDrawable);
        
        attributesArray.recycle();
        
        mPressedStateHelper = new PressedStateHelper();
        
        // By default Linearlayout that we extend is not drawn. This is
        // its draw() method is not called but dispatchDraw() is called
        // directly (see ViewGroup.drawChild()). However, this class uses
        // the fading edge effect implemented by View and we need our
        // draw() method to be called. Therefore, we declare we will draw.
        setWillNotDraw(!mHasSelectorWheel);
        
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(layoutResId, this, true);
        
        OnClickListener onClickListener = new OnClickListener()
        {
            public void onClick(View v)
            {
                hideSoftInput();
                mInputText.clearFocus();
                if (v.getId() == R.id.np__increment)
                {
                    changeValueByOne(true);
                }
                else
                {
                    changeValueByOne(false);
                }
            }
        };
        
        OnLongClickListener onLongClickListener = new OnLongClickListener()
        {
            public boolean onLongClick(View v)
            {
                hideSoftInput();
                mInputText.clearFocus();
                if (v.getId() == R.id.np__increment)
                {
                    postChangeCurrentByOneFromLongPress(true, 0);
                }
                else
                {
                    postChangeCurrentByOneFromLongPress(false, 0);
                }
                return true;
            }
        };
        
        // increment button
        if (!mHasSelectorWheel)
        {
            mIncrementButton = (ImageButton) findViewById(R.id.np__increment);
            mIncrementButton.setOnClickListener(onClickListener);
            mIncrementButton.setOnLongClickListener(onLongClickListener);
        }
        else
        {
            mIncrementButton = null;
        }
        
        // decrement button
        if (!mHasSelectorWheel)
        {
            mDecrementButton = (ImageButton) findViewById(R.id.np__decrement);
            mDecrementButton.setOnClickListener(onClickListener);
            mDecrementButton.setOnLongClickListener(onLongClickListener);
        }
        else
        {
            mDecrementButton = null;
        }
        
        // input text
        mInputText = (EditText) findViewById(R.id.np__numberpicker_input);
        mInputText.setOnFocusChangeListener(new OnFocusChangeListener()
        {
            public void onFocusChange(View v, boolean hasFocus)
            {
                if (hasFocus)
                {
                    mInputText.selectAll();
                }
                else
                {
                    mInputText.setSelection(0, 0);
                    validateInputTextView(v);
                }
            }
        });
        mInputText.setFilters(new InputFilter[] { new InputTextFilter() });
        
        mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
        mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
        
        // initialize constants
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
        mTextSize = (int) mInputText.getTextSize();
        
        // create the selector wheel paint
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setTextAlign(Align.CENTER);
        paint.setTextSize(mTextSize);
        paint.setTypeface(mInputText.getTypeface());
        ColorStateList colors = mInputText.getTextColors();
        int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
        paint.setColor(color);
        mSelectorWheelPaint = paint;
        
        // create the fling and adjust scrollers
        mFlingScroller = new Scroller(getContext(), null, true);
        mAdjustScroller = new Scroller(getContext(),
                new DecelerateInterpolator(2.5f));
        
        updateInputTextView();
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            // If not explicitly specified this view is important for accessibility.
            if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO)
            {
                setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
            }
        }
    }
    
    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom)
    {
        if (!mHasSelectorWheel)
        {
            super.onLayout(changed, left, top, right, bottom);
            return;
        }
        final int msrdWdth = getMeasuredWidth();
        final int msrdHght = getMeasuredHeight();
        
        // Input text centered horizontally.
        final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
        final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
        final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
        final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
        final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
        final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
        mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
        
        if (changed)
        {
            // need to do all this when we know our size
            initializeSelectorWheel();
            initializeFadingEdges();
            mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance)
                    / 2 - mSelectionDividerHeight;
            mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2
                    * mSelectionDividerHeight + mSelectionDividersDistance;
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        if (!mHasSelectorWheel)
        {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        // Try greedily to fit the max width and height.
        final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec,
                mMaxWidth);
        final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec,
                mMaxHeight);
        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
        // Flag if we are measured with width or height less than the respective min.
        final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth,
                getMeasuredWidth(),
                widthMeasureSpec);
        final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight,
                getMeasuredHeight(),
                heightMeasureSpec);
        setMeasuredDimension(widthSize, heightSize);
    }
    
    /**
     * Move to the final position of a scroller. Ensures to force finish the
     * scroller and if it is not at its final position a scroll of the selector
     * wheel is performed to fast forward to the final position.
     * 
     * @param scroller
     *            The scroller to whose final position to get.
     * @return True of the a move was performed, i.e. the scroller was not in
     *         final position.
     */
    private boolean moveToFinalScrollerPosition(Scroller scroller)
    {
        scroller.forceFinished(true);
        int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
        int futureScrollOffset = (mCurrentScrollOffset + amountToScroll)
                % mSelectorElementHeight;
        int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
        if (overshootAdjustment != 0)
        {
            if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2)
            {
                if (overshootAdjustment > 0)
                {
                    overshootAdjustment -= mSelectorElementHeight;
                }
                else
                {
                    overshootAdjustment += mSelectorElementHeight;
                }
            }
            amountToScroll += overshootAdjustment;
            scrollBy(0, amountToScroll);
            return true;
        }
        return false;
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event)
    {
        if (!mHasSelectorWheel || !isEnabled())
        {
            return false;
        }
        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
            {
                removeAllCallbacks();
                mInputText.setVisibility(View.INVISIBLE);
                mLastDownOrMoveEventY = mLastDownEventY = event.getY();
                mLastDownEventTime = event.getEventTime();
                mIngonreMoveEvents = false;
                mShowSoftInputOnTap = false;
                // Handle pressed state before any state change.
                if (mLastDownEventY < mTopSelectionDividerTop)
                {
                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE)
                    {
                        mPressedStateHelper.buttonPressDelayed(PressedStateHelper.BUTTON_DECREMENT);
                    }
                }
                else if (mLastDownEventY > mBottomSelectionDividerBottom)
                {
                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE)
                    {
                        mPressedStateHelper.buttonPressDelayed(PressedStateHelper.BUTTON_INCREMENT);
                    }
                }
                // Make sure we support flinging inside scrollables.
                getParent().requestDisallowInterceptTouchEvent(true);
                if (!mFlingScroller.isFinished())
                {
                    mFlingScroller.forceFinished(true);
                    mAdjustScroller.forceFinished(true);
                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                }
                else if (!mAdjustScroller.isFinished())
                {
                    mFlingScroller.forceFinished(true);
                    mAdjustScroller.forceFinished(true);
                }
                else if (mLastDownEventY < mTopSelectionDividerTop)
                {
                    hideSoftInput();
                    postChangeCurrentByOneFromLongPress(false,
                            ViewConfiguration.getLongPressTimeout());
                }
                else if (mLastDownEventY > mBottomSelectionDividerBottom)
                {
                    hideSoftInput();
                    postChangeCurrentByOneFromLongPress(true,
                            ViewConfiguration.getLongPressTimeout());
                }
                else
                {
                    mShowSoftInputOnTap = true;
                    postBeginSoftInputOnLongPressCommand();
                }
                return true;
            }
        }
        return false;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        if (!isEnabled() || !mHasSelectorWheel)
        {
            return false;
        }
        if (mVelocityTracker == null)
        {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action)
        {
            case MotionEvent.ACTION_MOVE:
            {
                if (mIngonreMoveEvents)
                {
                    break;
                }
                float currentMoveY = event.getY();
                if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL)
                {
                    int deltaDownY = (int) Math.abs(currentMoveY
                            - mLastDownEventY);
                    if (deltaDownY > mTouchSlop)
                    {
                        removeAllCallbacks();
                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                    }
                }
                else
                {
                    int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
                    scrollBy(0, deltaMoveY);
                    invalidate();
                }
                mLastDownOrMoveEventY = currentMoveY;
            }
                break;
            case MotionEvent.ACTION_UP:
            {
                removeBeginSoftInputCommand();
                removeChangeCurrentByOneFromLongPress();
                mPressedStateHelper.cancel();
                VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000,
                        mMaximumFlingVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity();
                if (Math.abs(initialVelocity) > mMinimumFlingVelocity)
                {
                    fling(initialVelocity);
                    onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                }
                else
                {
                    int eventY = (int) event.getY();
                    int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
                    long deltaTime = event.getEventTime() - mLastDownEventTime;
                    long tapTimeout = ViewConfiguration.getTapTimeout();
                    if (deltaMoveY <= mTouchSlop)
                    { // && deltaTime < ViewConfiguration.getTapTimeout()) {
                        if (mShowSoftInputOnTap)
                        {
                            mShowSoftInputOnTap = false;
                            showSoftInput();
                        }
                        else
                        {
                            int selectorIndexOffset = (eventY / mSelectorElementHeight)
                                    - SELECTOR_MIDDLE_ITEM_INDEX;
                            if (selectorIndexOffset > 0)
                            {
                                changeValueByOne(true);
                                mPressedStateHelper.buttonTapped(PressedStateHelper.BUTTON_INCREMENT);
                            }
                            else if (selectorIndexOffset < 0)
                            {
                                changeValueByOne(false);
                                mPressedStateHelper.buttonTapped(PressedStateHelper.BUTTON_DECREMENT);
                            }
                        }
                    }
                    else
                    {
                        ensureScrollWheelAdjusted();
                    }
                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                }
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
                break;
        }
        return true;
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action)
        {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                removeAllCallbacks();
                break;
        }
        return super.dispatchTouchEvent(event);
    }
    
    @Override
    public boolean dispatchKeyEvent(KeyEvent event)
    {
        final int keyCode = event.getKeyCode();
        switch (keyCode)
        {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                removeAllCallbacks();
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_UP:
                if (!mHasSelectorWheel)
                {
                    break;
                }
                switch (event.getAction())
                {
                    case KeyEvent.ACTION_DOWN:
                        if (mWrapSelectorWheel
                                || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) ? getValue() < getMaxValue()
                                : getValue() > getMinValue())
                        {
                            requestFocus();
                            mLastHandledDownDpadKeyCode = keyCode;
                            removeAllCallbacks();
                            if (mFlingScroller.isFinished())
                            {
                                changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
                            }
                            return true;
                        }
                        break;
                    case KeyEvent.ACTION_UP:
                        if (mLastHandledDownDpadKeyCode == keyCode)
                        {
                            mLastHandledDownDpadKeyCode = -1;
                            return true;
                        }
                        break;
                }
        }
        return super.dispatchKeyEvent(event);
    }
    
    @Override
    public boolean dispatchTrackballEvent(MotionEvent event)
    {
        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action)
        {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                removeAllCallbacks();
                break;
        }
        return super.dispatchTrackballEvent(event);
    }
    
    @Override
    protected boolean dispatchHoverEvent(MotionEvent event)
    {
        if (!mHasSelectorWheel)
        {
            return super.dispatchHoverEvent(event);
        }
        
        if (((AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled())
        {
            final int eventY = (int) event.getY();
            final int hoveredVirtualViewId;
            if (eventY < mTopSelectionDividerTop)
            {
                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
            }
            else if (eventY > mBottomSelectionDividerBottom)
            {
                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
            }
            else
            {
                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
            }
            final int action = event.getAction() & MotionEvent.ACTION_MASK;
            SupportAccessibilityNodeProvider provider = getSupportAccessibilityNodeProvider();
            
            switch (action)
            {
                case MotionEvent.ACTION_HOVER_ENTER:
                {
                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                            AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                    mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
                    provider.performAction(hoveredVirtualViewId,
                            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS,
                            null);
                }
                    break;
                case MotionEvent.ACTION_HOVER_MOVE:
                {
                    if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
                            && mLastHoveredChildVirtualViewId != View.NO_ID)
                    {
                        provider.sendAccessibilityEventForVirtualView(mLastHoveredChildVirtualViewId,
                                AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                        provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                                AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                        mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
                        provider.performAction(hoveredVirtualViewId,
                                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS,
                                null);
                    }
                }
                    break;
                case MotionEvent.ACTION_HOVER_EXIT:
                {
                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
                            AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
                    mLastHoveredChildVirtualViewId = View.NO_ID;
                }
                    break;
            }
        }
        return false;
    }
    
    @Override
    public void computeScroll()
    {
        Scroller scroller = mFlingScroller;
        if (scroller.isFinished())
        {
            scroller = mAdjustScroller;
            if (scroller.isFinished())
            {
                return;
            }
        }
        scroller.computeScrollOffset();
        int currentScrollerY = scroller.getCurrY();
        if (mPreviousScrollerY == 0)
        {
            mPreviousScrollerY = scroller.getStartY();
        }
        scrollBy(0, currentScrollerY - mPreviousScrollerY);
        mPreviousScrollerY = currentScrollerY;
        if (scroller.isFinished())
        {
            onScrollerFinished(scroller);
        }
        else
        {
            invalidate();
        }
    }
    
    @Override
    public void setEnabled(boolean enabled)
    {
        super.setEnabled(enabled);
        if (!mHasSelectorWheel)
        {
            mIncrementButton.setEnabled(enabled);
        }
        if (!mHasSelectorWheel)
        {
            mDecrementButton.setEnabled(enabled);
        }
        mInputText.setEnabled(enabled);
    }
    
    @Override
    public void scrollBy(int x, int y)
    {
        int[] selectorIndices = mSelectorIndices;
        if (!mWrapSelectorWheel && y > 0
                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue)
        {
            mCurrentScrollOffset = mInitialScrollOffset;
            return;
        }
        if (!mWrapSelectorWheel && y < 0
                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue)
        {
            mCurrentScrollOffset = mInitialScrollOffset;
            return;
        }
        mCurrentScrollOffset += y;
        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight)
        {
            mCurrentScrollOffset -= mSelectorElementHeight;
            decrementSelectorIndices(selectorIndices);
            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
            if (!mWrapSelectorWheel
                    && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue)
            {
                mCurrentScrollOffset = mInitialScrollOffset;
            }
        }
        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight)
        {
            mCurrentScrollOffset += mSelectorElementHeight;
            incrementSelectorIndices(selectorIndices);
            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
            if (!mWrapSelectorWheel
                    && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue)
            {
                mCurrentScrollOffset = mInitialScrollOffset;
            }
        }
    }
    
    @Override
    public int getSolidColor()
    {
        return mSolidColor;
    }
    
    /**
     * Sets the listener to be notified on change of the current value.
     * 
     * @param onValueChangedListener
     *            The listener.
     */
    public void setOnValueChangedListener(
            OnValueChangeListener onValueChangedListener)
    {
        mOnValueChangeListener = onValueChangedListener;
    }
    
    /**
     * Set listener to be notified for scroll state changes.
     * 
     * @param onScrollListener
     *            The listener.
     */
    public void setOnScrollListener(OnScrollListener onScrollListener)
    {
        mOnScrollListener = onScrollListener;
    }
    
    /**
     * Set the formatter to be used for formatting the current value.
     * <p>
     * Note: If you have provided alternative values for the values this
     * formatter is never invoked.
     * </p>
     * 
     * @param formatter
     *            The formatter object. If formatter is <code>null</code>,
     *            {@link String#valueOf(int)} will be used.
     * @see #setDisplayedValues(String[])
     */
    public void setFormatter(Formatter formatter)
    {
        if (formatter == mFormatter)
        {
            return;
        }
        mFormatter = formatter;
        initializeSelectorWheelIndices();
        updateInputTextView();
    }
    
    /**
     * Set the current value for the number picker.
     * <p>
     * If the argument is less than the {@link NumberPicker#getMinValue()} and
     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
     * current value is set to the {@link NumberPicker#getMinValue()} value.
     * </p>
     * <p>
     * If the argument is less than the {@link NumberPicker#getMinValue()} and
     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
     * current value is set to the {@link NumberPicker#getMaxValue()} value.
     * </p>
     * <p>
     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
     * current value is set to the {@link NumberPicker#getMaxValue()} value.
     * </p>
     * <p>
     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
     * current value is set to the {@link NumberPicker#getMinValue()} value.
     * </p>
     * 
     * @param value
     *            The current value.
     * @see #setWrapSelectorWheel(boolean)
     * @see #setMinValue(int)
     * @see #setMaxValue(int)
     */
    public void setValue(int value)
    {
        setValueInternal(value, false);
    }
    
    /**
     * Shows the soft input for its input text.
     */
    private void showSoftInput()
    {
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null)
        {
            if (mHasSelectorWheel)
            {
                mInputText.setVisibility(View.VISIBLE);
            }
            mInputText.requestFocus();
            inputMethodManager.showSoftInput(mInputText, 0);
        }
    }
    
    /**
     * Hides the soft input if it is active for the input text.
     */
    private void hideSoftInput()
    {
        InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (inputMethodManager != null
                && inputMethodManager.isActive(mInputText))
        {
            inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
            if (mHasSelectorWheel)
            {
                mInputText.setVisibility(View.INVISIBLE);
            }
        }
    }
    
    /**
     * Computes the max width if no such specified as an attribute.
     */
    private void tryComputeMaxWidth()
    {
        if (!mComputeMaxWidth)
        {
            return;
        }
        int maxTextWidth = 0;
        if (mDisplayedValues == null)
        {
            float maxDigitWidth = 0;
            for (int i = 0; i <= 9; i++)
            {
                final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
                if (digitWidth > maxDigitWidth)
                {
                    maxDigitWidth = digitWidth;
                }
            }
            int numberOfDigits = 0;
            int current = mMaxValue;
            while (current > 0)
            {
                numberOfDigits++;
                current = current / 10;
            }
            maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
        }
        else
        {
            final int valueCount = mDisplayedValues.length;
            for (int i = 0; i < valueCount; i++)
            {
                final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
                if (textWidth > maxTextWidth)
                {
                    maxTextWidth = (int) textWidth;
                }
            }
        }
        maxTextWidth += mInputText.getPaddingLeft()
                + mInputText.getPaddingRight();
        if (mMaxWidth != maxTextWidth)
        {
            if (maxTextWidth > mMinWidth)
            {
                mMaxWidth = maxTextWidth;
            }
            else
            {
                mMaxWidth = mMinWidth;
            }
            invalidate();
        }
    }
    
    /**
     * Gets whether the selector wheel wraps when reaching the min/max value.
     * 
     * @return True if the selector wheel wraps.
     * 
     * @see #getMinValue()
     * @see #getMaxValue()
     */
    public boolean getWrapSelectorWheel()
    {
        return mWrapSelectorWheel;
    }
    
    /**
     * Sets whether the selector wheel shown during flinging/scrolling should
     * wrap around the {@link NumberPicker#getMinValue()} and
     * {@link NumberPicker#getMaxValue()} values.
     * <p>
     * By default if the range (max - min) is more than the number of items
     * shown on the selector wheel the selector wheel wrapping is enabled.
     * </p>
     * <p>
     * <strong>Note:</strong> If the number of items, i.e. the range (
     * {@link #getMaxValue()} - {@link #getMinValue()}) is less than the number
     * of items shown on the selector wheel, the selector wheel will not wrap.
     * Hence, in such a case calling this method is a NOP.
     * </p>
     * 
     * @param wrapSelectorWheel
     *            Whether to wrap.
     */
    public void setWrapSelectorWheel(boolean wrapSelectorWheel)
    {
        final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
        if ((!wrapSelectorWheel || wrappingAllowed)
                && wrapSelectorWheel != mWrapSelectorWheel)
        {
            mWrapSelectorWheel = wrapSelectorWheel;
        }
    }
    
    /**
     * Sets the speed at which the numbers be incremented and decremented when
     * the up and down buttons are long pressed respectively.
     * <p>
     * The default value is 300 ms.
     * </p>
     * 
     * @param intervalMillis
     *            The speed (in milliseconds) at which the numbers will be
     *            incremented and decremented.
     */
    public void setOnLongPressUpdateInterval(long intervalMillis)
    {
        mLongPressUpdateInterval = intervalMillis;
    }
    
    /**
     * Returns the value of the picker.
     * 
     * @return The value.
     */
    public int getValue()
    {
        return mValue;
    }
    
    /**
     * Returns the min value of the picker.
     * 
     * @return The min value
     */
    public int getMinValue()
    {
        return mMinValue;
    }
    
    /**
     * Sets the min value of the picker.
     * 
     * @param minValue
     *            The min value inclusive.
     * 
     *            <strong>Note:</strong> The length of the displayed values
     *            array set via {@link #setDisplayedValues(String[])} must be
     *            equal to the range of selectable numbers which is equal to
     *            {@link #getMaxValue()} - {@link #getMinValue()} + 1.
     */
    public void setMinValue(int minValue)
    {
        if (mMinValue == minValue)
        {
            return;
        }
        if (minValue < 0)
        {
            throw new IllegalArgumentException("minValue must be >= 0");
        }
        mMinValue = minValue;
        if (mMinValue > mValue)
        {
            mValue = mMinValue;
        }
        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
        setWrapSelectorWheel(wrapSelectorWheel);
        initializeSelectorWheelIndices();
        updateInputTextView();
        tryComputeMaxWidth();
        invalidate();
    }
    
    /**
     * Returns the max value of the picker.
     * 
     * @return The max value.
     */
    public int getMaxValue()
    {
        return mMaxValue;
    }
    
    /**
     * Sets the max value of the picker.
     * 
     * @param maxValue
     *            The max value inclusive.
     * 
     *            <strong>Note:</strong> The length of the displayed values
     *            array set via {@link #setDisplayedValues(String[])} must be
     *            equal to the range of selectable numbers which is equal to
     *            {@link #getMaxValue()} - {@link #getMinValue()} + 1.
     */
    public void setMaxValue(int maxValue)
    {
        if (mMaxValue == maxValue)
        {
            return;
        }
        if (maxValue < 0)
        {
            throw new IllegalArgumentException("maxValue must be >= 0");
        }
        mMaxValue = maxValue;
        if (mMaxValue < mValue)
        {
            mValue = mMaxValue;
        }
        boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
        setWrapSelectorWheel(wrapSelectorWheel);
        initializeSelectorWheelIndices();
        updateInputTextView();
        tryComputeMaxWidth();
        invalidate();
    }
    
    /**
     * Gets the values to be displayed instead of string values.
     * 
     * @return The displayed values.
     */
    public String[] getDisplayedValues()
    {
        return mDisplayedValues;
    }
    
    /**
     * Sets the values to be displayed.
     * 
     * @param displayedValues
     *            The displayed values.
     * 
     *            <strong>Note:</strong> The length of the displayed values
     *            array must be equal to the range of selectable numbers which
     *            is equal to {@link #getMaxValue()} - {@link #getMinValue()} +
     *            1.
     */
    public void setDisplayedValues(String[] displayedValues)
    {
        if (mDisplayedValues == displayedValues)
        {
            return;
        }
        mDisplayedValues = displayedValues;
        if (mDisplayedValues != null)
        {
            // Allow text entry rather than strictly numeric entry.
            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
        }
        else
        {
            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
        }
        updateInputTextView();
        initializeSelectorWheelIndices();
        tryComputeMaxWidth();
    }
    
    @Override
    protected float getTopFadingEdgeStrength()
    {
        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
    }
    
    @Override
    protected float getBottomFadingEdgeStrength()
    {
        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
    }
    
    @Override
    protected void onDetachedFromWindow()
    {
        removeAllCallbacks();
    }
    
    @Override
    protected void onDraw(Canvas canvas)
    {
        if (!mHasSelectorWheel)
        {
            super.onDraw(canvas);
            return;
        }
        float x = (getRight() - getLeft()) / 2;
        float y = mCurrentScrollOffset;
        
        // draw the virtual buttons pressed state if needed
        if (mVirtualButtonPressedDrawable != null
                && mScrollState == OnScrollListener.SCROLL_STATE_IDLE)
        {
            if (mDecrementVirtualButtonPressed)
            {
                //mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
                mVirtualButtonPressedDrawable.setState(PRESSED_ENABLED_STATE_SET);
                mVirtualButtonPressedDrawable.setBounds(0,
                        0,
                        getRight(),
                        mTopSelectionDividerTop);
                mVirtualButtonPressedDrawable.draw(canvas);
            }
            if (mIncrementVirtualButtonPressed)
            {
                //mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
                mVirtualButtonPressedDrawable.setState(PRESSED_ENABLED_STATE_SET);
                mVirtualButtonPressedDrawable.setBounds(0,
                        mBottomSelectionDividerBottom,
                        getRight(),
                        getBottom());
                mVirtualButtonPressedDrawable.draw(canvas);
            }
        }
        
        // draw the selector wheel
        int[] selectorIndices = mSelectorIndices;
        for (int i = 0; i < selectorIndices.length; i++)
        {
            int selectorIndex = selectorIndices[i];
            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
            // Do not draw the middle item if input is visible since the input
            // is shown only if the wheel is static and it covers the middle
            // item. Otherwise, if the user starts editing the text via the
            // IME he may see a dimmed version of the old value intermixed
            // with the new one.
            if (i != SELECTOR_MIDDLE_ITEM_INDEX
                    || mInputText.getVisibility() != VISIBLE)
            {
                canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
            }
            y += mSelectorElementHeight;
        }
        
        // draw the selection dividers
        if (mSelectionDivider != null)
        {
            // draw the top divider
            int topOfTopDivider = mTopSelectionDividerTop;
            int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
            mSelectionDivider.setBounds(0,
                    topOfTopDivider,
                    getRight(),
                    bottomOfTopDivider);
            mSelectionDivider.draw(canvas);
            
            // draw the bottom divider
            int bottomOfBottomDivider = mBottomSelectionDividerBottom;
            int topOfBottomDivider = bottomOfBottomDivider
                    - mSelectionDividerHeight;
            mSelectionDivider.setBounds(0,
                    topOfBottomDivider,
                    getRight(),
                    bottomOfBottomDivider);
            mSelectionDivider.draw(canvas);
        }
    }
    
    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event)
    {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(NumberPicker.class.getName());
        event.setScrollable(true);
        event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
        event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
    }
    
    @Override
    public AccessibilityNodeProvider getAccessibilityNodeProvider()
    {
        if (!mHasSelectorWheel)
        {
            return super.getAccessibilityNodeProvider();
        }
        if (mAccessibilityNodeProvider == null)
        {
            mAccessibilityNodeProvider = new SupportAccessibilityNodeProvider();
        }
        return mAccessibilityNodeProvider.mProvider;
    }
    
    /**
     * Makes a measure spec that tries greedily to use the max value.
     * 
     * @param measureSpec
     *            The measure spec.
     * @param maxSize
     *            The max value for the size.
     * @return A measure spec greedily imposing the max size.
     */
    private int makeMeasureSpec(int measureSpec, int maxSize)
    {
        if (maxSize == SIZE_UNSPECIFIED)
        {
            return measureSpec;
        }
        final int size = MeasureSpec.getSize(measureSpec);
        final int mode = MeasureSpec.getMode(measureSpec);
        switch (mode)
        {
            case MeasureSpec.EXACTLY:
                return measureSpec;
            case MeasureSpec.AT_MOST:
                return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize),
                        MeasureSpec.EXACTLY);
            case MeasureSpec.UNSPECIFIED:
                return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
            default:
                throw new IllegalArgumentException("Unknown measure mode: "
                        + mode);
        }
    }
    
    /**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Tries to respect the min size, unless a different size
     * is imposed by the constraints.
     * 
     * @param minSize
     *            The minimal desired size.
     * @param measuredSize
     *            The currently measured size.
     * @param measureSpec
     *            The current measure spec.
     * @return The resolved size and state.
     */
    private int resolveSizeAndStateRespectingMinSize(int minSize,
            int measuredSize, int measureSpec)
    {
        if (minSize != SIZE_UNSPECIFIED)
        {
            final int desiredWidth = Math.max(minSize, measuredSize);
            return resolveSizeAndState(desiredWidth, measureSpec, 0);
        }
        else
        {
            return measuredSize;
        }
    }
    
    /**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size is
     * imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
     * size is smaller than the size the view wants to be.
     * 
     * @param size
     *            How big the view wants to be
     * @param measureSpec
     *            Constraints imposed by the parent
     * @return Size information bit mask as defined by
     *         {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}
     *         .
     */
    public static int resolveSizeAndState(int size, int measureSpec,
            int childMeasuredState)
    {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode)
        {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                if (specSize < size)
                {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                }
                else
                {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }
    
    /**
     * Resets the selector indices and clear the cached string representation of
     * these indices.
     */
    private void initializeSelectorWheelIndices()
    {
        mSelectorIndexToStringCache.clear();
        int[] selectorIndices = mSelectorIndices;
        int current = getValue();
        for (int i = 0; i < mSelectorIndices.length; i++)
        {
            int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
            if (mWrapSelectorWheel)
            {
                selectorIndex = getWrappedSelectorIndex(selectorIndex);
            }
            selectorIndices[i] = selectorIndex;
            ensureCachedScrollSelectorValue(selectorIndices[i]);
        }
    }
    
    /**
     * Sets the current value of this NumberPicker.
     * 
     * @param current
     *            The new value of the NumberPicker.
     * @param notifyChange
     *            Whether to notify if the current value changed.
     */
    private void setValueInternal(int current, boolean notifyChange)
    {
        if (mValue == current)
        {
            return;
        }
        // Wrap around the values if we go past the start or end
        if (mWrapSelectorWheel)
        {
            current = getWrappedSelectorIndex(current);
        }
        else
        {
            current = Math.max(current, mMinValue);
            current = Math.min(current, mMaxValue);
        }
        int previous = mValue;
        mValue = current;
        updateInputTextView();
        if (notifyChange)
        {
            notifyChange(previous, current);
        }
        initializeSelectorWheelIndices();
        invalidate();
    }
    
    /**
     * Changes the current value by one which is increment or decrement based on
     * the passes argument. decrement the current value.
     * 
     * @param increment
     *            True to increment, false to decrement.
     */
    private void changeValueByOne(boolean increment)
    {
        if (mHasSelectorWheel)
        {
            mInputText.setVisibility(View.INVISIBLE);
            if (!moveToFinalScrollerPosition(mFlingScroller))
            {
                moveToFinalScrollerPosition(mAdjustScroller);
            }
            mPreviousScrollerY = 0;
            if (increment)
            {
                mFlingScroller.startScroll(0,
                        0,
                        0,
                        -mSelectorElementHeight,
                        SNAP_SCROLL_DURATION);
            }
            else
            {
                mFlingScroller.startScroll(0,
                        0,
                        0,
                        mSelectorElementHeight,
                        SNAP_SCROLL_DURATION);
            }
            invalidate();
        }
        else
        {
            if (increment)
            {
                setValueInternal(mValue + 1, true);
            }
            else
            {
                setValueInternal(mValue - 1, true);
            }
        }
    }
    
    private void initializeSelectorWheel()
    {
        initializeSelectorWheelIndices();
        int[] selectorIndices = mSelectorIndices;
        int totalTextHeight = selectorIndices.length * mTextSize;
        float totalTextGapHeight = (getBottom() - getTop()) - totalTextHeight;
        float textGapCount = selectorIndices.length;
        mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
        mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
        // Ensure that the middle item is positioned the same as the text in
        // mInputText
        int editTextTextPosition = mInputText.getBaseline()
                + mInputText.getTop();
        mInitialScrollOffset = editTextTextPosition
                - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
        mCurrentScrollOffset = mInitialScrollOffset;
        updateInputTextView();
    }
    
    private void initializeFadingEdges()
    {
        setVerticalFadingEdgeEnabled(true);
        setFadingEdgeLength((getBottom() - getTop() - mTextSize) / 2);
    }
    
    /**
     * Callback invoked upon completion of a given <code>scroller</code>.
     */
    private void onScrollerFinished(Scroller scroller)
    {
        if (scroller == mFlingScroller)
        {
            if (!ensureScrollWheelAdjusted())
            {
                updateInputTextView();
            }
            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        }
        else
        {
            if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL)
            {
                updateInputTextView();
            }
        }
    }
    
    /**
     * Handles transition to a given <code>scrollState</code>
     */
    private void onScrollStateChange(int scrollState)
    {
        if (mScrollState == scrollState)
        {
            return;
        }
        mScrollState = scrollState;
        if (mOnScrollListener != null)
        {
            mOnScrollListener.onScrollStateChange(this, scrollState);
        }
    }
    
    /**
     * Flings the selector with the given <code>velocityY</code>.
     */
    private void fling(int velocityY)
    {
        mPreviousScrollerY = 0;
        
        if (velocityY > 0)
        {
            mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
        }
        else
        {
            mFlingScroller.fling(0,
                    Integer.MAX_VALUE,
                    0,
                    velocityY,
                    0,
                    0,
                    0,
                    Integer.MAX_VALUE);
        }
        
        invalidate();
    }
    
    /**
     * @return The wrapped index <code>selectorIndex</code> value.
     */
    private int getWrappedSelectorIndex(int selectorIndex)
    {
        if (selectorIndex > mMaxValue)
        {
            return mMinValue + (selectorIndex - mMaxValue)
                    % (mMaxValue - mMinValue) - 1;
        }
        else if (selectorIndex < mMinValue)
        {
            return mMaxValue - (mMinValue - selectorIndex)
                    % (mMaxValue - mMinValue) + 1;
        }
        return selectorIndex;
    }
    
    /**
     * Increments the <code>selectorIndices</code> whose string representations
     * will be displayed in the selector.
     */
    private void incrementSelectorIndices(int[] selectorIndices)
    {
        for (int i = 0; i < selectorIndices.length - 1; i++)
        {
            selectorIndices[i] = selectorIndices[i + 1];
        }
        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
        if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue)
        {
            nextScrollSelectorIndex = mMinValue;
        }
        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
    }
    
    /**
     * Decrements the <code>selectorIndices</code> whose string representations
     * will be displayed in the selector.
     */
    private void decrementSelectorIndices(int[] selectorIndices)
    {
        for (int i = selectorIndices.length - 1; i > 0; i--)
        {
            selectorIndices[i] = selectorIndices[i - 1];
        }
        int nextScrollSelectorIndex = selectorIndices[1] - 1;
        if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue)
        {
            nextScrollSelectorIndex = mMaxValue;
        }
        selectorIndices[0] = nextScrollSelectorIndex;
        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
    }
    
    /**
     * Ensures we have a cached string representation of the given <code>
     * selectorIndex</code> to avoid multiple instantiations of the same string.
     */
    private void ensureCachedScrollSelectorValue(int selectorIndex)
    {
        SparseArray<String> cache = mSelectorIndexToStringCache;
        String scrollSelectorValue = cache.get(selectorIndex);
        if (scrollSelectorValue != null)
        {
            return;
        }
        if (selectorIndex < mMinValue || selectorIndex > mMaxValue)
        {
            scrollSelectorValue = "";
        }
        else
        {
            if (mDisplayedValues != null)
            {
                int displayedValueIndex = selectorIndex - mMinValue;
                scrollSelectorValue = mDisplayedValues[displayedValueIndex];
            }
            else
            {
                scrollSelectorValue = formatNumber(selectorIndex);
            }
        }
        cache.put(selectorIndex, scrollSelectorValue);
    }
    
    private String formatNumber(int value)
    {
        return (mFormatter != null) ? mFormatter.format(value)
                : formatNumberWithLocale(value);
    }
    
    private void validateInputTextView(View v)
    {
        String str = String.valueOf(((TextView) v).getText());
        if (TextUtils.isEmpty(str))
        {
            // Restore to the old value as we don't allow empty values
            updateInputTextView();
        }
        else
        {
            // Check the new value and ensure it's in range
            int current = getSelectedPos(str.toString());
            setValueInternal(current, true);
        }
    }
    
    /**
     * Updates the view of this NumberPicker. If displayValues were specified in
     * the string corresponding to the index specified by the current value will
     * be returned. Otherwise, the formatter specified in {@link #setFormatter}
     * will be used to format the number.
     * 
     * @return Whether the text was updated.
     */
    private boolean updateInputTextView()
    {
        /*
         * If we don't have displayed values then use the current number else
         * find the correct value in the displayed values for the current
         * number.
         */
        String text = (mDisplayedValues == null) ? formatNumber(mValue)
                : mDisplayedValues[mValue - mMinValue];
        if (!TextUtils.isEmpty(text)
                && !text.equals(mInputText.getText().toString()))
        {
            mInputText.setText(text);
            return true;
        }
        
        return false;
    }
    
    /**
     * Notifies the listener, if registered, of a change of the value of this
     * NumberPicker.
     */
    private void notifyChange(int previous, int current)
    {
        if (mOnValueChangeListener != null)
        {
            mOnValueChangeListener.onValueChange(this, previous, mValue);
        }
    }
    
    /**
     * Posts a command for changing the current value by one.
     * 
     * @param increment
     *            Whether to increment or decrement the value.
     */
    private void postChangeCurrentByOneFromLongPress(boolean increment,
            long delayMillis)
    {
        if (mChangeCurrentByOneFromLongPressCommand == null)
        {
            mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
        }
        else
        {
            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
        }
        mChangeCurrentByOneFromLongPressCommand.setStep(increment);
        postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
    }
    
    /**
     * Removes the command for changing the current value by one.
     */
    private void removeChangeCurrentByOneFromLongPress()
    {
        if (mChangeCurrentByOneFromLongPressCommand != null)
        {
            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
        }
    }
    
    /**
     * Posts a command for beginning an edit of the current value via IME on
     * long press.
     */
    private void postBeginSoftInputOnLongPressCommand()
    {
        if (mBeginSoftInputOnLongPressCommand == null)
        {
            mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
        }
        else
        {
            removeCallbacks(mBeginSoftInputOnLongPressCommand);
        }
        postDelayed(mBeginSoftInputOnLongPressCommand,
                ViewConfiguration.getLongPressTimeout());
    }
    
    /**
     * Removes the command for beginning an edit of the current value via IME.
     */
    private void removeBeginSoftInputCommand()
    {
        if (mBeginSoftInputOnLongPressCommand != null)
        {
            removeCallbacks(mBeginSoftInputOnLongPressCommand);
        }
    }
    
    /**
     * Removes all pending callback from the message queue.
     */
    private void removeAllCallbacks()
    {
        if (mChangeCurrentByOneFromLongPressCommand != null)
        {
            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
        }
        if (mSetSelectionCommand != null)
        {
            removeCallbacks(mSetSelectionCommand);
        }
        if (mBeginSoftInputOnLongPressCommand != null)
        {
            removeCallbacks(mBeginSoftInputOnLongPressCommand);
        }
        mPressedStateHelper.cancel();
    }
    
    /**
     * @return The selected index given its displayed <code>value</code>.
     */
    private int getSelectedPos(String value)
    {
        if (mDisplayedValues == null)
        {
            try
            {
                return Integer.parseInt(value);
            }
            catch (NumberFormatException e)
            {
                // Ignore as if it's not a number we don't care
            }
        }
        else
        {
            for (int i = 0; i < mDisplayedValues.length; i++)
            {
                // Don't force the user to type in jan when ja will do
                value = value.toLowerCase();
                if (mDisplayedValues[i].toLowerCase().startsWith(value))
                {
                    return mMinValue + i;
                }
            }
            
            /*
             * The user might have typed in a number into the month field i.e.
             * 10 instead of OCT so support that too.
             */
            try
            {
                return Integer.parseInt(value);
            }
            catch (NumberFormatException e)
            {
                
                // Ignore as if it's not a number we don't care
            }
        }
        return mMinValue;
    }
    
    /**
     * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
     * </code> to <code>selectionEnd</code>.
     */
    private void postSetSelectionCommand(int selectionStart, int selectionEnd)
    {
        if (mSetSelectionCommand == null)
        {
            mSetSelectionCommand = new SetSelectionCommand();
        }
        else
        {
            removeCallbacks(mSetSelectionCommand);
        }
        mSetSelectionCommand.mSelectionStart = selectionStart;
        mSetSelectionCommand.mSelectionEnd = selectionEnd;
        post(mSetSelectionCommand);
    }
    
    /**
     * The numbers accepted by the input text's {@link Filter}
     */
    private static final char[] DIGIT_CHARACTERS = new char[] {
            // Latin digits are the common case
            '0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9',
            // Arabic-Indic
            '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665',
            '\u0666', '\u0667', '\u0668', '\u0669',
            // Extended Arabic-Indic
            '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5',
            '\u06f6', '\u06f7', '\u06f8', '\u06f9' };
    
    /**
     * Filter for accepting only valid indices or prefixes of the string
     * representation of valid indices.
     */
    class InputTextFilter extends NumberKeyListener
    {
        
        // XXX This doesn't allow for range limits when controlled by a
        // soft input method!
        public int getInputType()
        {
            return InputType.TYPE_CLASS_TEXT;
        }
        
        @Override
        protected char[] getAcceptedChars()
        {
            return DIGIT_CHARACTERS;
        }
        
        @Override
        public CharSequence filter(CharSequence source, int start, int end,
                Spanned dest, int dstart, int dend)
        {
            if (mDisplayedValues == null)
            {
                CharSequence filtered = super.filter(source,
                        start,
                        end,
                        dest,
                        dstart,
                        dend);
                if (filtered == null)
                {
                    filtered = source.subSequence(start, end);
                }
                
                String result = String.valueOf(dest.subSequence(0, dstart))
                        + filtered + dest.subSequence(dend, dest.length());
                
                if ("".equals(result))
                {
                    return result;
                }
                int val = getSelectedPos(result);
                
                /*
                 * Ensure the user can't type in a value greater than the max
                 * allowed. We have to allow less than min as the user might
                 * want to delete some numbers and then type a new number.
                 */
                if (val > mMaxValue)
                {
                    return "";
                }
                else
                {
                    return filtered;
                }
            }
            else
            {
                CharSequence filtered = String.valueOf(source.subSequence(start,
                        end));
                if (TextUtils.isEmpty(filtered))
                {
                    return "";
                }
                String result = String.valueOf(dest.subSequence(0, dstart))
                        + filtered + dest.subSequence(dend, dest.length());
                String str = String.valueOf(result).toLowerCase();
                for (String val : mDisplayedValues)
                {
                    String valLowerCase = val.toLowerCase();
                    if (valLowerCase.startsWith(str))
                    {
                        postSetSelectionCommand(result.length(), val.length());
                        return val.subSequence(dstart, val.length());
                    }
                }
                return "";
            }
        }
    }
    
    /**
     * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
     * middle element is in the middle of the widget.
     * 
     * @return Whether an adjustment has been made.
     */
    private boolean ensureScrollWheelAdjusted()
    {
        // adjust to the closest value
        int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
        if (deltaY != 0)
        {
            mPreviousScrollerY = 0;
            if (Math.abs(deltaY) > mSelectorElementHeight / 2)
            {
                deltaY += (deltaY > 0) ? -mSelectorElementHeight
                        : mSelectorElementHeight;
            }
            mAdjustScroller.startScroll(0,
                    0,
                    0,
                    deltaY,
                    SELECTOR_ADJUSTMENT_DURATION_MILLIS);
            invalidate();
            return true;
        }
        return false;
    }
    
    class PressedStateHelper implements Runnable
    {
        public static final int BUTTON_INCREMENT = 1;
        
        public static final int BUTTON_DECREMENT = 2;
        
        private final int MODE_PRESS = 1;
        
        private final int MODE_TAPPED = 2;
        
        private int mManagedButton;
        
        private int mMode;
        
        public void cancel()
        {
            mMode = 0;
            mManagedButton = 0;
            NumberPicker.this.removeCallbacks(this);
            if (mIncrementVirtualButtonPressed)
            {
                mIncrementVirtualButtonPressed = false;
                invalidate(0,
                        mBottomSelectionDividerBottom,
                        getRight(),
                        getBottom());
            }
            mDecrementVirtualButtonPressed = false;
            if (mDecrementVirtualButtonPressed)
            {
                invalidate(0, 0, getRight(), mTopSelectionDividerTop);
            }
        }
        
        public void buttonPressDelayed(int button)
        {
            cancel();
            mMode = MODE_PRESS;
            mManagedButton = button;
            NumberPicker.this.postDelayed(this,
                    ViewConfiguration.getTapTimeout());
        }
        
        public void buttonTapped(int button)
        {
            cancel();
            mMode = MODE_TAPPED;
            mManagedButton = button;
            NumberPicker.this.post(this);
        }
        
        @Override
        public void run()
        {
            switch (mMode)
            {
                case MODE_PRESS:
                {
                    switch (mManagedButton)
                    {
                        case BUTTON_INCREMENT:
                        {
                            mIncrementVirtualButtonPressed = true;
                            invalidate(0,
                                    mBottomSelectionDividerBottom,
                                    getRight(),
                                    getBottom());
                        }
                            break;
                        case BUTTON_DECREMENT:
                        {
                            mDecrementVirtualButtonPressed = true;
                            invalidate(0,
                                    0,
                                    getRight(),
                                    mTopSelectionDividerTop);
                        }
                    }
                }
                    break;
                case MODE_TAPPED:
                {
                    switch (mManagedButton)
                    {
                        case BUTTON_INCREMENT:
                        {
                            if (!mIncrementVirtualButtonPressed)
                            {
                                NumberPicker.this.postDelayed(this,
                                        ViewConfiguration.getPressedStateDuration());
                            }
                            mIncrementVirtualButtonPressed ^= true;
                            invalidate(0,
                                    mBottomSelectionDividerBottom,
                                    getRight(),
                                    getBottom());
                        }
                            break;
                        case BUTTON_DECREMENT:
                        {
                            if (!mDecrementVirtualButtonPressed)
                            {
                                NumberPicker.this.postDelayed(this,
                                        ViewConfiguration.getPressedStateDuration());
                            }
                            mDecrementVirtualButtonPressed ^= true;
                            invalidate(0,
                                    0,
                                    getRight(),
                                    mTopSelectionDividerTop);
                        }
                    }
                }
                    break;
            }
        }
    }
    
    /**
     * Command for setting the input text selection.
     */
    class SetSelectionCommand implements Runnable
    {
        private int mSelectionStart;
        
        private int mSelectionEnd;
        
        public void run()
        {
            mInputText.setSelection(mSelectionStart, mSelectionEnd);
        }
    }
    
    /**
     * Command for changing the current value from a long press by one.
     */
    class ChangeCurrentByOneFromLongPressCommand implements Runnable
    {
        private boolean mIncrement;
        
        private void setStep(boolean increment)
        {
            mIncrement = increment;
        }
        
        @Override
        public void run()
        {
            changeValueByOne(mIncrement);
            postDelayed(this, mLongPressUpdateInterval);
        }
    }
    
    /**
     * @hide
     */
    public static class CustomEditText extends EditText
    {
        
        public CustomEditText(Context context, AttributeSet attrs)
        {
            super(context, attrs);
        }
        
        @Override
        public void onEditorAction(int actionCode)
        {
            super.onEditorAction(actionCode);
            if (actionCode == EditorInfo.IME_ACTION_DONE)
            {
                clearFocus();
            }
        }
    }
    
    /**
     * Command for beginning soft input on long press.
     */
    class BeginSoftInputOnLongPressCommand implements Runnable
    {
        
        @Override
        public void run()
        {
            showSoftInput();
            mIngonreMoveEvents = true;
        }
    }
    
    private SupportAccessibilityNodeProvider getSupportAccessibilityNodeProvider()
    {
        return new SupportAccessibilityNodeProvider();
    }
    
    class SupportAccessibilityNodeProvider
    {
        
        AccessibilityNodeProviderImpl mProvider;
        
        private SupportAccessibilityNodeProvider()
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
            {
                mProvider = new AccessibilityNodeProviderImpl();
            }
        }
        
        public boolean performAction(int virtualViewId, int action,
                Bundle arguments)
        {
            if (mProvider != null)
            {
                return mProvider.performAction(virtualViewId, action, arguments);
            }
            
            return false;
        }
        
        public void sendAccessibilityEventForVirtualView(int virtualViewId,
                int eventType)
        {
            if (mProvider != null)
                mProvider.sendAccessibilityEventForVirtualView(virtualViewId,
                        eventType);
        }
    }
    
    /**
     * Class for managing virtual view tree rooted at this picker.
     */
    class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider
    {
        private static final int UNDEFINED = Integer.MIN_VALUE;
        
        private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
        
        private static final int VIRTUAL_VIEW_ID_INPUT = 2;
        
        private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
        
        private final Rect mTempRect = new Rect();
        
        private final int[] mTempArray = new int[2];
        
        private int mAccessibilityFocusedView = UNDEFINED;
        
        @Override
        public AccessibilityNodeInfo createAccessibilityNodeInfo(
                int virtualViewId)
        {
            switch (virtualViewId)
            {
                case View.NO_ID:
                    return createAccessibilityNodeInfoForNumberPicker(getScrollX(),
                            getScrollY(),
                            getScrollX() + (getRight() - getLeft()),
                            getScrollY() + (getBottom() - getTop()));
                case VIRTUAL_VIEW_ID_DECREMENT:
                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
                            getVirtualDecrementButtonText(),
                            getScrollX(),
                            getScrollY(),
                            getScrollX() + (getRight() - getLeft()),
                            mTopSelectionDividerTop + mSelectionDividerHeight);
                case VIRTUAL_VIEW_ID_INPUT:
                    return createAccessibiltyNodeInfoForInputText();
                case VIRTUAL_VIEW_ID_INCREMENT:
                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
                            getVirtualIncrementButtonText(),
                            getScrollX(),
                            mBottomSelectionDividerBottom
                                    - mSelectionDividerHeight,
                            getScrollX() + (getRight() - getLeft()),
                            getScrollY() + (getBottom() - getTop()));
            }
            return super.createAccessibilityNodeInfo(virtualViewId);
        }
        
        @Override
        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(
                String searched, int virtualViewId)
        {
            if (TextUtils.isEmpty(searched))
            {
                return Collections.emptyList();
            }
            String searchedLowerCase = searched.toLowerCase();
            List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
            switch (virtualViewId)
            {
                case View.NO_ID:
                {
                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                            VIRTUAL_VIEW_ID_DECREMENT,
                            result);
                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                            VIRTUAL_VIEW_ID_INPUT,
                            result);
                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                            VIRTUAL_VIEW_ID_INCREMENT,
                            result);
                    return result;
                }
                case VIRTUAL_VIEW_ID_DECREMENT:
                case VIRTUAL_VIEW_ID_INCREMENT:
                case VIRTUAL_VIEW_ID_INPUT:
                {
                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
                            virtualViewId,
                            result);
                    return result;
                }
            }
            return super.findAccessibilityNodeInfosByText(searched,
                    virtualViewId);
        }
        
        @Override
        public boolean performAction(int virtualViewId, int action,
                Bundle arguments)
        {
            switch (virtualViewId)
            {
                case View.NO_ID:
                {
                    switch (action)
                    {
                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView != virtualViewId)
                            {
                                mAccessibilityFocusedView = virtualViewId;
                                // requestAccessibilityFocus();
                                performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS,
                                        null);
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView == virtualViewId)
                            {
                                mAccessibilityFocusedView = UNDEFINED;
                                // clearAccessibilityFocus();
                                performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
                                        null);
                                return true;
                            }
                            return false;
                        }
                        case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
                        {
                            if (NumberPicker.this.isEnabled()
                                    && (getWrapSelectorWheel() || getValue() < getMaxValue()))
                            {
                                changeValueByOne(true);
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
                        {
                            if (NumberPicker.this.isEnabled()
                                    && (getWrapSelectorWheel() || getValue() > getMinValue()))
                            {
                                changeValueByOne(false);
                                return true;
                            }
                        }
                            return false;
                    }
                }
                    break;
                case VIRTUAL_VIEW_ID_INPUT:
                {
                    switch (action)
                    {
                        case AccessibilityNodeInfo.ACTION_FOCUS:
                        {
                            if (NumberPicker.this.isEnabled()
                                    && !mInputText.isFocused())
                            {
                                return mInputText.requestFocus();
                            }
                        }
                            break;
                        case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS:
                        {
                            if (NumberPicker.this.isEnabled()
                                    && mInputText.isFocused())
                            {
                                mInputText.clearFocus();
                                return true;
                            }
                            return false;
                        }
                        case AccessibilityNodeInfo.ACTION_CLICK:
                        {
                            if (NumberPicker.this.isEnabled())
                            {
                                showSoftInput();
                                return true;
                            }
                            return false;
                        }
                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView != virtualViewId)
                            {
                                mAccessibilityFocusedView = virtualViewId;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                mInputText.invalidate();
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView == virtualViewId)
                            {
                                mAccessibilityFocusedView = UNDEFINED;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                mInputText.invalidate();
                                return true;
                            }
                        }
                            return false;
                        default:
                        {
                            return mInputText.performAccessibilityAction(action,
                                    arguments);
                        }
                    }
                }
                    return false;
                case VIRTUAL_VIEW_ID_INCREMENT:
                {
                    switch (action)
                    {
                        case AccessibilityNodeInfo.ACTION_CLICK:
                        {
                            if (NumberPicker.this.isEnabled())
                            {
                                NumberPicker.this.changeValueByOne(true);
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView != virtualViewId)
                            {
                                mAccessibilityFocusedView = virtualViewId;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                invalidate(0,
                                        mBottomSelectionDividerBottom,
                                        getRight(),
                                        getBottom());
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView == virtualViewId)
                            {
                                mAccessibilityFocusedView = UNDEFINED;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                invalidate(0,
                                        mBottomSelectionDividerBottom,
                                        getRight(),
                                        getBottom());
                                return true;
                            }
                        }
                            return false;
                    }
                }
                    return false;
                case VIRTUAL_VIEW_ID_DECREMENT:
                {
                    switch (action)
                    {
                        case AccessibilityNodeInfo.ACTION_CLICK:
                        {
                            if (NumberPicker.this.isEnabled())
                            {
                                final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
                                NumberPicker.this.changeValueByOne(increment);
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView != virtualViewId)
                            {
                                mAccessibilityFocusedView = virtualViewId;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                invalidate(0,
                                        0,
                                        getRight(),
                                        mTopSelectionDividerTop);
                                return true;
                            }
                        }
                            return false;
                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
                        {
                            if (mAccessibilityFocusedView == virtualViewId)
                            {
                                mAccessibilityFocusedView = UNDEFINED;
                                sendAccessibilityEventForVirtualView(virtualViewId,
                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
                                invalidate(0,
                                        0,
                                        getRight(),
                                        mTopSelectionDividerTop);
                                return true;
                            }
                        }
                            return false;
                    }
                }
                    return false;
            }
            return super.performAction(virtualViewId, action, arguments);
        }
        
        public void sendAccessibilityEventForVirtualView(int virtualViewId,
                int eventType)
        {
            switch (virtualViewId)
            {
                case VIRTUAL_VIEW_ID_DECREMENT:
                {
                    if (hasVirtualDecrementButton())
                    {
                        sendAccessibilityEventForVirtualButton(virtualViewId,
                                eventType,
                                getVirtualDecrementButtonText());
                    }
                }
                    break;
                case VIRTUAL_VIEW_ID_INPUT:
                {
                    sendAccessibilityEventForVirtualText(eventType);
                }
                    break;
                case VIRTUAL_VIEW_ID_INCREMENT:
                {
                    if (hasVirtualIncrementButton())
                    {
                        sendAccessibilityEventForVirtualButton(virtualViewId,
                                eventType,
                                getVirtualIncrementButtonText());
                    }
                }
                    break;
            }
        }
        
        private void sendAccessibilityEventForVirtualText(int eventType)
        {
            if (((AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled())
            {
                AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
                mInputText.onInitializeAccessibilityEvent(event);
                mInputText.onPopulateAccessibilityEvent(event);
                event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
                requestSendAccessibilityEvent(NumberPicker.this, event);
            }
        }
        
        private void sendAccessibilityEventForVirtualButton(int virtualViewId,
                int eventType, String text)
        {
            if (((AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled())
            {
                AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
                event.setClassName(Button.class.getName());
                event.setPackageName(getContext().getPackageName());
                event.getText().add(text);
                event.setEnabled(NumberPicker.this.isEnabled());
                event.setSource(NumberPicker.this, virtualViewId);
                requestSendAccessibilityEvent(NumberPicker.this, event);
            }
        }
        
        private void findAccessibilityNodeInfosByTextInChild(
                String searchedLowerCase, int virtualViewId,
                List<AccessibilityNodeInfo> outResult)
        {
            switch (virtualViewId)
            {
                case VIRTUAL_VIEW_ID_DECREMENT:
                {
                    String text = getVirtualDecrementButtonText();
                    if (!TextUtils.isEmpty(text)
                            && text.toString()
                                    .toLowerCase()
                                    .contains(searchedLowerCase))
                    {
                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
                    }
                }
                    return;
                case VIRTUAL_VIEW_ID_INPUT:
                {
                    CharSequence text = mInputText.getText();
                    if (!TextUtils.isEmpty(text)
                            && text.toString()
                                    .toLowerCase()
                                    .contains(searchedLowerCase))
                    {
                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
                        return;
                    }
                    CharSequence contentDesc = mInputText.getText();
                    if (!TextUtils.isEmpty(contentDesc)
                            && contentDesc.toString()
                                    .toLowerCase()
                                    .contains(searchedLowerCase))
                    {
                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
                        return;
                    }
                }
                    break;
                case VIRTUAL_VIEW_ID_INCREMENT:
                {
                    String text = getVirtualIncrementButtonText();
                    if (!TextUtils.isEmpty(text)
                            && text.toString()
                                    .toLowerCase()
                                    .contains(searchedLowerCase))
                    {
                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
                    }
                }
                    return;
            }
        }
        
        private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText()
        {
            AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
            info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
            if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
            }
            if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
            }
            return info;
        }
        
        private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(
                int virtualViewId, String text, int left, int top, int right,
                int bottom)
        {
            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
            info.setClassName(Button.class.getName());
            info.setPackageName(getContext().getPackageName());
            info.setSource(NumberPicker.this, virtualViewId);
            info.setParent(NumberPicker.this);
            info.setText(text);
            info.setClickable(true);
            info.setLongClickable(true);
            info.setEnabled(NumberPicker.this.isEnabled());
            Rect boundsInParent = mTempRect;
            boundsInParent.set(left, top, right, bottom);
            // TODO info.setVisibleToUser(isVisibleToUser(boundsInParent));
            info.setBoundsInParent(boundsInParent);
            Rect boundsInScreen = boundsInParent;
            int[] locationOnScreen = mTempArray;
            getLocationOnScreen(locationOnScreen);
            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
            info.setBoundsInScreen(boundsInScreen);
            
            if (mAccessibilityFocusedView != virtualViewId)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
            }
            if (mAccessibilityFocusedView == virtualViewId)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
            }
            if (NumberPicker.this.isEnabled())
            {
                info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
            }
            
            return info;
        }
        
        private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(
                int left, int top, int right, int bottom)
        {
            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
            info.setClassName(NumberPicker.class.getName());
            info.setPackageName(getContext().getPackageName());
            info.setSource(NumberPicker.this);
            
            if (hasVirtualDecrementButton())
            {
                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
            }
            info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
            if (hasVirtualIncrementButton())
            {
                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
            }
            
            info.setParent((View) getParentForAccessibility());
            info.setEnabled(NumberPicker.this.isEnabled());
            info.setScrollable(true);
            
            /**
             * TODO: Figure out compat implementation for this final float
             * applicationScale =
             * getContext().getResources().getCompatibilityInfo
             * ().applicationScale;
             * 
             * Rect boundsInParent = mTempRect; boundsInParent.set(left, top,
             * right, bottom); boundsInParent.scale(applicationScale);
             * info.setBoundsInParent(boundsInParent);
             * 
             * info.setVisibleToUser(isVisibleToUser());
             * 
             * Rect boundsInScreen = boundsInParent; int[] locationOnScreen =
             * mTempArray; getLocationOnScreen(locationOnScreen);
             * boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
             * boundsInScreen.scale(applicationScale);
             * info.setBoundsInScreen(boundsInScreen);
             */
            
            if (mAccessibilityFocusedView != View.NO_ID)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
            }
            if (mAccessibilityFocusedView == View.NO_ID)
            {
                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
            }
            if (NumberPicker.this.isEnabled())
            {
                if (getWrapSelectorWheel() || getValue() < getMaxValue())
                {
                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                }
                if (getWrapSelectorWheel() || getValue() > getMinValue())
                {
                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
                }
            }
            
            return info;
        }
        
        private boolean hasVirtualDecrementButton()
        {
            return getWrapSelectorWheel() || getValue() > getMinValue();
        }
        
        private boolean hasVirtualIncrementButton()
        {
            return getWrapSelectorWheel() || getValue() < getMaxValue();
        }
        
        private String getVirtualDecrementButtonText()
        {
            int value = mValue - 1;
            if (mWrapSelectorWheel)
            {
                value = getWrappedSelectorIndex(value);
            }
            if (value >= mMinValue)
            {
                return (mDisplayedValues == null) ? formatNumber(value)
                        : mDisplayedValues[value - mMinValue];
            }
            return null;
        }
        
        private String getVirtualIncrementButtonText()
        {
            int value = mValue + 1;
            if (mWrapSelectorWheel)
            {
                value = getWrappedSelectorIndex(value);
            }
            if (value <= mMaxValue)
            {
                return (mDisplayedValues == null) ? formatNumber(value)
                        : mDisplayedValues[value - mMinValue];
            }
            return null;
        }
    }
    
    static private String formatNumberWithLocale(int value)
    {
        return String.format(Locale.getDefault(), "%d", value);
    }
}
